2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import <Adium/AIEmoticon.h>
18 #import <Adium/AIEmoticonPack.h>
19 #import <Adium/AIEmoticonControllerProtocol.h>
20 #import <AIUtilities/AIFileManagerAdditions.h>
21 #import <AIUtilities/AIImageAdditions.h>
23 #define EMOTICON_PATH_EXTENSION @"emoticon"
24 #define EMOTICON_PACK_TEMP_EXTENSION @"AdiumEmoticonOld"
26 #define EMOTICON_PLIST_FILENAME @"Emoticons.plist"
27 #define EMOTICON_PACK_VERSION @"AdiumSetVersion"
28 #define EMOTICON_LIST @"Emoticons"
30 #define EMOTICON_EQUIVALENTS @"Equivalents"
31 #define EMOTICON_NAME @"Name"
33 #define EMOTICON_SERVICE_CLASS @"Service Class"
35 #define EMOTICON_LOCATION @"Location"
36 #define EMOTICON_LOCATION_SEPARATOR @"////"
38 @interface AIEmoticonPack (PRIVATE)
39 - (AIEmoticonPack *)initFromPath:(NSString *)inPath;
40 - (void)setEmoticonArray:(NSArray *)inArray;
41 - (void)loadEmoticons;
42 - (void)loadAdiumEmoticons:(NSDictionary *)emoticons localizedStrings:(NSDictionary *)localizationDict;
43 - (void)loadProteusEmoticons:(NSDictionary *)emoticons;
44 - (void)_upgradeEmoticonPack:(NSString *)packPath;
45 - (NSString *)_imagePathForEmoticonPath:(NSString *)inPath;
46 - (NSArray *)_equivalentsForEmoticonPath:(NSString *)inPath;
47 - (NSString *)_stringWithMacEndlines:(NSString *)inString;
52 * @class AIEmoticonPack
53 * @brief Class to encapsulate an emoticon pack, which is a themed collection of emoticons
55 * An emoticon pack must have a name and a set of one or more emoticons (AIEmoticon objects).
56 * It may also have a serviceClass, which indicates the class of a service upon which its emoticons are preferred.
57 * For example, a set of MSN emoticons would have a service class of @"MSN".
59 @implementation AIEmoticonPack
62 * @brief Create a new emoticon pack
63 * @param inPath The path to the root of a bundle of emoticons
65 + (id)emoticonPackFromPath:(NSString *)inPath
67 return [[[self alloc] initFromPath:inPath] autorelease];
71 - (AIEmoticonPack *)initFromPath:(NSString *)inPath
73 if ((self = [super init])) {
74 path = [inPath retain];
76 bundle = [[NSBundle bundleWithPath:path] retain];
79 if (xtraBundle && ([[xtraBundle objectForInfoDictionaryKey:@"XtraBundleVersion"] intValue] == 1)) {
80 //This checks for a new-style xtra
81 //New style xtras store the same info, but it's in Contents/Resources/ so that we can have an info.plist file and use NSBundle.
82 emoticonLocation = [[xtraBundle resourcePath] retain];
86 NSString *localizedName;
87 name = [[path lastPathComponent] stringByDeletingPathExtension];
88 if ((localizedName = [[bundle localizedInfoDictionary] objectForKey:name])) {
94 enabledEmoticonArray = nil;
108 [emoticonArray release];
109 [enabledEmoticonArray release];
110 [serviceClass release];
116 * @brief Name, for display to the user
124 * @brief Path to this emoticon pack
132 * @brief Service class of this emoticon pack
134 * @result A service class, or nil if the emoticon pack is not associated with any service class
136 - (NSString *)serviceClass
142 * @brief An array of AIEmoticon objects
144 - (NSArray *)emoticons
146 if (!emoticonArray) [self loadEmoticons];
147 return emoticonArray;
151 * @brief An array of enabled AIEmoticon objects
153 - (NSArray *)enabledEmoticons
155 NSEnumerator *enumerator;
158 if (!enabledEmoticonArray) {
159 enabledEmoticonArray = [[NSMutableArray alloc] init];
160 enumerator = [[self emoticons] objectEnumerator];
161 while ((emo = [enumerator nextObject])) {
163 [enabledEmoticonArray addObject:emo];
167 return enabledEmoticonArray;
171 * @brief Return the preview image to use within a menu for this emoticon
173 * It tries to be the emoticon for text equivalent :) or :-). Failing that, any emoticon will do.
175 - (NSImage *)menuPreviewImage
177 NSArray *myEmoticons = [self emoticons];
178 NSEnumerator *enumerator;
179 AIEmoticon *emoticon;
181 enumerator = [myEmoticons objectEnumerator];
182 while ((emoticon = [enumerator nextObject])) {
183 NSArray *equivalents = [emoticon textEquivalents];
184 if ([equivalents containsObject:@":)"] || [equivalents containsObject:@":-)"]) {
189 //If we didn't find a happy emoticon, use the first one in the array
190 if (!emoticon && [myEmoticons count]) {
191 emoticon = [myEmoticons objectAtIndex:0];
194 return [[emoticon image] imageByScalingForMenuItem];
198 * @brief Set the emoticons that are disabled in this pack
199 * @param inArray An NSArray of AIEmoticon objects to disable
201 - (void)setDisabledEmoticons:(NSArray *)inArray
203 NSEnumerator *enumerator;
204 AIEmoticon *emoticon;
206 //Flag our emoticons as enabled/disabled
207 enumerator = [[self emoticons] objectEnumerator];
208 while ((emoticon = [enumerator nextObject])) {
209 [emoticon setEnabled:(![inArray containsObject:[emoticon name]])];
212 //reset the emabled emoticon list
213 if (enabledEmoticonArray) {
214 [enabledEmoticonArray release];
215 enabledEmoticonArray = nil;
220 * @brief Enable/Disable this pack
221 * @param inEnabled Should this pack be enabled?
223 - (void)setIsEnabled:(BOOL)inEnabled
229 * @brief Is this pack enabled?
235 //Copying --------------------------------------------------------------------------------------------------------------
238 - (id)copyWithZone:(NSZone *)zone
240 AIEmoticonPack *newPack = [[AIEmoticonPack alloc] initFromPath:path];
242 newPack->emoticonArray = [emoticonArray mutableCopy];
243 newPack->serviceClass = [serviceClass retain];
244 newPack->path = [path retain];
245 newPack->bundle = [bundle retain];
246 newPack->name = [name retain];
251 //Loading Emoticons ----------------------------------------------------------------------------------------------------
252 #pragma mark Loading Emoticons
254 * @brief Load the emoticons in this pack.
256 * Called by [self emoticons] as needed
258 - (void)loadEmoticons
260 [emoticonArray release]; emoticonArray = [[NSMutableArray alloc] init];
261 [serviceClass release]; serviceClass = nil;
264 NSString *infoDictPath = [[bundle resourcePath] stringByAppendingPathComponent:EMOTICON_PLIST_FILENAME];
265 NSDictionary *infoDict = [NSDictionary dictionaryWithContentsOfFile:infoDictPath];
266 NSDictionary *localizedInfoDict = [[NSBundle bundleWithPath:path] localizedInfoDictionary];
268 //If no info dict was found, assume that this is an old emoticon pack and try to upgrade it
270 [self _upgradeEmoticonPack:path];
271 infoDict = [NSDictionary dictionaryWithContentsOfFile:infoDictPath];
272 [bundle release]; bundle = [[NSBundle bundleWithPath:path] retain];
277 /* Handle optional location key, which allows emoticons to be loaded
278 * from arbitrary directories. This is only used by the iChat emoticon
281 id possiblePaths = [infoDict objectForKey:EMOTICON_LOCATION];
283 if ([possiblePaths isKindOfClass:[NSString class]]) {
284 possiblePaths = [NSArray arrayWithObjects:possiblePaths, nil];
287 NSEnumerator *pathEnumerator = [possiblePaths objectEnumerator];
290 while ((aPath = [pathEnumerator nextObject])) {
291 NSString *possiblePath;
292 NSArray *splitPath = [aPath componentsSeparatedByString:EMOTICON_LOCATION_SEPARATOR];
294 /* Two possible formats:
296 * <string>/absolute/path/to/directory</string>
297 * <string>CFBundleIdentifier////relative/path/from/bundle/to/directory</string>
299 * The separator in the latter is ////, defined as EMOTICON_LOCATION_SEPARATOR.
301 if ([splitPath count] == 1) {
302 possiblePath = [splitPath objectAtIndex:0];
304 NSArray *components = [NSArray arrayWithObjects:
305 [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:[splitPath objectAtIndex:0]],
306 [splitPath objectAtIndex:1],
308 possiblePath = [NSString pathWithComponents:components];
311 /* If the directory exists, then we've found the location. If we
312 * make it all the way through the list without finding a valid
313 * directory, then the standard location will be used.
316 if ([[NSFileManager defaultManager] fileExistsAtPath:possiblePath isDirectory:&isDir] && isDir) {
318 bundle = [[NSBundle bundleWithPath:possiblePath] retain];
324 int version = [[infoDict objectForKey:EMOTICON_PACK_VERSION] intValue];
327 case 0: [self loadProteusEmoticons:infoDict]; break;
328 case 1: [self loadAdiumEmoticons:[infoDict objectForKey:EMOTICON_LIST] localizedStrings:localizedInfoDict]; break;
332 serviceClass = [[infoDict objectForKey:EMOTICON_SERVICE_CLASS] retain];
334 if ([name rangeOfString:@"AIM"].location != NSNotFound) {
335 serviceClass = [@"AIM-compatible" retain];
336 } else if ([name rangeOfString:@"MSN"].location != NSNotFound) {
337 serviceClass = [@"MSN" retain];
338 } else if ([name rangeOfString:@"Yahoo"].location != NSNotFound) {
339 serviceClass = [@"Yahoo!" retain];
344 //Sort the emoticons in this pack using the AIEmoticon compare: selector
345 [emoticonArray sortUsingSelector:@selector(compare:)];
349 * @brief Load an Adium version 1 emoticon pack
351 * @param emoticons A dictionary whose keys are file names and objects are themselves dictionaries with equivalent and name information.
353 - (void)loadAdiumEmoticons:(NSDictionary *)emoticons localizedStrings:(NSDictionary *)localizationDict
355 NSEnumerator *enumerator = [emoticons keyEnumerator];
357 NSBundle *myBundle = (!localizationDict ? [NSBundle bundleForClass:[self class]] : nil);
359 while ((fileName = [enumerator nextObject])) {
360 id dict = [emoticons objectForKey:fileName];
362 if ([dict isKindOfClass:[NSDictionary class]]) {
363 NSString *emoticonName = [(NSDictionary *)dict objectForKey:EMOTICON_NAME];
364 NSString *localizedEmoticonName = nil;
367 if (localizationDict) {
368 //If the bundle provides localizations, use them
369 localizedEmoticonName = [localizationDict objectForKey:emoticonName];
372 if (!localizedEmoticonName) {
373 //Otherwise, look at our list of default translations (generated at the bottom of this file)
374 localizedEmoticonName = [myBundle localizedStringForKey:emoticonName
376 table:@"EmoticonNames"];
379 if (localizedEmoticonName)
380 emoticonName = localizedEmoticonName;
383 [emoticonArray addObject:[AIEmoticon emoticonWithIconPath:[bundle pathForImageResource:fileName]
384 equivalents:[(NSDictionary *)dict objectForKey:EMOTICON_EQUIVALENTS]
392 * @brief Load a Proteus emoticon pack
394 - (void)loadProteusEmoticons:(NSDictionary *)emoticons
396 NSEnumerator *enumerator = [emoticons keyEnumerator];
399 while ((fileName = [enumerator nextObject])) {
400 NSDictionary *dict = [emoticons objectForKey:fileName];
402 [emoticonArray addObject:[AIEmoticon emoticonWithIconPath:[bundle pathForImageResource:fileName]
403 equivalents:[dict objectForKey:@"String Representations"]
404 name:[dict objectForKey:@"Meaning"]
410 * @brief Flush any cached emoticon images (and image attachment strings)
412 - (void)flushEmoticonImageCache
414 NSEnumerator *enumerator;
415 AIEmoticon *emoticon;
417 //Flag our emoticons as enabled/disabled
418 enumerator = [[self emoticons] objectEnumerator];
419 while ((emoticon = [enumerator nextObject])) {
420 [emoticon flushEmoticonImageCache];
425 //Upgrading ------------------------------------------------------------------------------------------------------------
426 //Methods for opening and converting old format Adium emoticon packs
427 #pragma mark Upgrading
429 * @brief Upgrade an emoticon pack from the old format (where every emoticon is a separate file) to the new format
431 - (void)_upgradeEmoticonPack:(NSString *)packPath
433 NSString *packName, *workingDirectory, *tempPackName, *tempPackPath, *fileName;
434 NSDirectoryEnumerator *enumerator;
435 NSFileManager *mgr = [NSFileManager defaultManager];
436 NSMutableDictionary *infoDict = [NSMutableDictionary dictionary];
437 NSMutableDictionary *emoticonDict = [NSMutableDictionary dictionary];
440 packName = [[packPath lastPathComponent] stringByDeletingPathExtension];
441 workingDirectory = [packPath stringByDeletingLastPathComponent];
443 //Rename the existing pack to .AdiumEmoticonOld
444 tempPackName = [packName stringByAppendingPathExtension:EMOTICON_PACK_TEMP_EXTENSION];
445 tempPackPath = [workingDirectory stringByAppendingPathComponent:tempPackName];
446 [mgr movePath:packPath toPath:tempPackPath handler:nil];
448 //Create ourself a new pack
449 [mgr createDirectoryAtPath:packPath attributes:nil];
451 //Version this pack as 1
452 [infoDict setObject:[NSNumber numberWithInt:1] forKey:EMOTICON_PACK_VERSION];
454 //Process all .emoticons in the old pack
455 enumerator = [[NSFileManager defaultManager] enumeratorAtPath:tempPackPath];
456 while ((fileName = [enumerator nextObject])) {
457 if ([[fileName lastPathComponent] characterAtIndex:0] != '.' &&
458 [[fileName pathExtension] caseInsensitiveCompare:EMOTICON_PATH_EXTENSION] == 0) {
459 NSString *emoticonPath = [tempPackPath stringByAppendingPathComponent:fileName];
462 //Ensure that this is a folder and that it is non-empty
463 [mgr fileExistsAtPath:emoticonPath isDirectory:&isDirectory];
465 NSString *emoticonName = [fileName stringByDeletingPathExtension];
467 //Get the text equivalents out of this .emoticon
468 NSArray *emoticonStrings = [self _equivalentsForEmoticonPath:emoticonPath];
470 //Get the image out of this .emoticon
471 NSString *imagePath = [self _imagePathForEmoticonPath:emoticonPath];
472 NSString *imageExtension = [imagePath pathExtension];
474 if (emoticonStrings && imagePath) {
475 NSString *newImageName = [emoticonName stringByAppendingPathExtension:imageExtension];
477 //Move the image into our new pack (with a unique name)
478 NSString *newImagePath = [packPath stringByAppendingPathComponent:newImageName];
479 [mgr copyPath:imagePath toPath:newImagePath handler:nil];
481 //Add to our emoticon plist
482 [emoticonDict setObject:[NSDictionary dictionaryWithObjectsAndKeys:
483 emoticonStrings, EMOTICON_EQUIVALENTS,
484 emoticonName, EMOTICON_NAME, nil]
485 forKey:newImageName];
491 //Write our plist to the new pack
492 [infoDict setObject:emoticonDict forKey:EMOTICON_LIST];
493 [infoDict writeToFile:[packPath stringByAppendingPathComponent:EMOTICON_PLIST_FILENAME] atomically:NO];
495 //Move the old/temp pack to the trash
496 [mgr trashFileAtPath:tempPackPath];
500 * @brief Path to an emoticon image
502 * @param Path within which to search for a file whose name starts with "Emoticon"
504 - (NSString *)_imagePathForEmoticonPath:(NSString *)inPath
506 NSDirectoryEnumerator *enumerator;
509 //Search for the file named Emoticon in our bundle (It can be in any image format)
510 enumerator = [[NSFileManager defaultManager] enumeratorAtPath:inPath];
511 while ((fileName = [enumerator nextObject])) {
512 if ([fileName hasPrefix:@"Emoticon"]) return [inPath stringByAppendingPathComponent:fileName];
519 * @brief Retrieve the text equivalents from a pack
521 - (NSArray *)_equivalentsForEmoticonPath:(NSString *)inPath
523 NSString *equivFilePath = [inPath stringByAppendingPathComponent:@"TextEquivalents.txt"];
524 NSArray *textEquivalents = nil;
526 //Fetch the text equivalents
527 if ([[NSFileManager defaultManager] fileExistsAtPath:equivFilePath]) {
528 NSString *equivString;
530 //Convert the text file into an array of strings
531 equivString = [NSMutableString stringWithContentsOfFile:equivFilePath];
532 equivString = [self _stringWithMacEndlines:equivString];
533 textEquivalents = [[equivString componentsSeparatedByString:@"\r"] retain];
536 return textEquivalents;
540 * @brief Convert any unix/windows line endings to mac line endings
541 * @result The converted string
543 - (NSString *)_stringWithMacEndlines:(NSString *)inString
545 NSCharacterSet *newlineSet = [NSCharacterSet characterSetWithCharactersInString:@"\n"];
546 NSMutableString *newString = nil; //We avoid creating a new string if not necessary
549 //Step through all the invalid endlines
550 charRange = [inString rangeOfCharacterFromSet:newlineSet];
551 while (charRange.length != 0) {
552 if (!newString) newString = [[inString mutableCopy] autorelease];
554 //Replace endline and continue
555 [newString replaceCharactersInRange:charRange withString:@"\r"];
556 charRange = [newString rangeOfCharacterFromSet:newlineSet];
559 return newString ? newString : inString;
562 - (NSString *)description
564 return ([NSString stringWithFormat:@"[%@: %@, ServiceClass %@]",[super description], [self name], [self serviceClass]]);
567 /* Localized emoticon names, listed here for genstrings:
569 AILocalizedStringFromTable(@"Angry", "EmoticonNames", "Emoticon name")
570 AILocalizedStringFromTable(@"Blush", "EmoticonNames", "Emoticon name")
571 AILocalizedStringFromTable(@"Cry", "EmoticonNames", "Emoticon name")
572 AILocalizedStringFromTable(@"Scared", "EmoticonNames", "Emoticon name")
573 AILocalizedStringFromTable(@"Sad", "EmoticonNames", "Emoticon name")
574 AILocalizedStringFromTable(@"Gasp", "EmoticonNames", "Emoticon name")
575 AILocalizedStringFromTable(@"Grin", "EmoticonNames", "Emoticon name")
576 AILocalizedStringFromTable(@"Angel", "EmoticonNames", "Emoticon name")
577 AILocalizedStringFromTable(@"Kiss", "EmoticonNames", "Emoticon name")
578 AILocalizedStringFromTable(@"Lips Are Sealed", "EmoticonNames", "Emoticon name")
579 AILocalizedStringFromTable(@"Money-mouth", "EmoticonNames", "Emoticon name")
580 AILocalizedStringFromTable(@"Smile", "EmoticonNames", "Emoticon name")
581 AILocalizedStringFromTable(@"Sticking Out Tongue", "EmoticonNames", "Emoticon name")
582 AILocalizedStringFromTable(@"Erm", "EmoticonNames", "Emoticon name")
583 AILocalizedStringFromTable(@"Cool", "EmoticonNames", "Emoticon name")
584 AILocalizedStringFromTable(@"Wink", "EmoticonNames", "Emoticon name")
585 AILocalizedStringFromTable(@"Foot In Mouth", "EmoticonNames", "Emoticon name")
586 AILocalizedStringFromTable(@"Frown", "EmoticonNames", "Emoticon name")
587 AILocalizedStringFromTable(@"Confused", "EmoticonNames", "Emoticon name")
588 AILocalizedStringFromTable(@"Halo", "EmoticonNames", "Emoticon name")
589 AILocalizedStringFromTable(@"Undecided", "EmoticonNames", "Emoticon name")
590 AILocalizedStringFromTable(@"Embarrassed", "EmoticonNames", "Emoticon name")